/*
 * Copyright (c) 2024 Shenzhen Kaihong Digital Industry Development Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { InputStream } from './InputStream'
import { DfuBaseService } from '../DfuBaseService'
import { Manifest, FileInfo, ManifestFile } from './Manifest'

import fs from '@ohos.file.fs';
import zlib from '@ohos.zlib';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { WorkerMsg } from '../WorkMsg';

export class ArchiveInputStream extends InputStream {
  // private DOMAIN: number = 0x8632

  // The name of the manifest file is fixed.
  public static MANIFEST: string = "manifest.json";
  // Those file names are for backwards compatibility mode
  public static SOFTDEVICE_HEX: string = "softdevice.hex";
  public static SOFTDEVICE_BIN: string = "softdevice.bin";
  public static BOOTLOADER_HEX: string = "bootloader.hex";
  public static BOOTLOADER_BIN: string = "bootloader.bin";
  public static APPLICATION_HEX: string = "application.hex";
  public static APPLICATION_BIN: string = "application.bin";
  public static SYSTEM_INIT: string = "system.dat";
  public static APPLICATION_INIT: string = "application.dat";

  private zipInputStream: fs.File;

  // Contains bytes arrays with BIN files. HEX files are converted to BIN before being

  private entries: Map<string, Uint8Array>;
  private crc32: number;
  private manifest: Manifest;
  private wmsg: WorkerMsg;
  
  private applicationBytes: Uint8Array;
  private softDeviceBytes: Uint8Array;
  private bootloaderBytes: Uint8Array;
  private softDeviceAndBootloaderBytes: Uint8Array;
  private systemInitBytes: Uint8Array;
  private applicationInitBytes: Uint8Array;
  private currentSource: Uint8Array;
  private type: number;
  private bytesReadFromCurrentSource: number;
  private softDeviceSize: number;
  private bootloaderSize: number;
  private applicationSize: number;
  private bytesRead: number;
  
  private markedSource: Uint8Array;
  private bytesReadFromMarkedSource: number;
  public updateBuf: Uint8Array = new Uint8Array();
  public updateCrc: number = 0;
  
  constructor(fsHandle: fs.File, mbrSize: number, types: number, wmsg: WorkerMsg) {
    super(wmsg.applicationBytes);
    this.TAG = 'ArchiveInputStream';
    this.fstat = fs.statSync(fsHandle.fd);
    hilog.info(this.DOMAIN, this.TAG, 'ArchiveInputStream construct');
    // Check if the file is not too big. It's hard to say what would be considered too big, but
    // 10 MB should be enough for any firmware package. If not, fork the library and change this.
    if (this.fstat.size > 10 * 1024 * 1024) {
      hilog.error(this.DOMAIN, this.TAG, `File too large: ${this.fstat.size} bytes (max 10 MB)`);
    }

    // TODO: need zip method too open the ZIP file
    // this.zipInputStream = new ZipInputStream(stream);

    this.crc32 = 0;
    this.entries = new Map<string, Uint8Array>();
    this.bytesRead = 0;
    this.bytesReadFromCurrentSource = 0;

    try {
      //TODO: read all entries from ZIP and puts them to entries map.
      //TODO: If manifest.json exists, convert to manifestData string.
      this.parseZip(wmsg, mbrSize);

      // if manifest is not null, assume the struct like this
      // {
      //   "manifest": {
      //     "application": {
      //       "bin_file": "nrf52832_HA_App.bin",
      //       "dat_file": "nrf52832_HA_App.dat"
      //     }
      //     "bootloader": {
      //       "bin_file": "nrf52832_HA_App.bin",
      //       "dat_file": "nrf52832_HA_App.dat"
      //     }
      //     "softdevice": {
      //       "bin_file": "nrf52832_HA_App.bin",
      //       "dat_file": "nrf52832_HA_App.dat"
      //     }
      //   }
      // }
      let valid: boolean = false;
      if (this.manifest != null) {
        //TODO: Read the application
        if (this.manifest.application) {
          let binFile = this.manifest.application.bin_file;
          let datFile = this.manifest.application.dat_file;
          hilog.info(this.DOMAIN, this.TAG, `application manifest: ${binFile}, ${datFile}`);
          //TODO: read the binFile and datFile from zip
          this.applicationBytes = this.wmsg.binBuf;
          this.applicationInitBytes = this.wmsg.datBuf;

          if (this.applicationBytes == null) {
            hilog.error(this.DOMAIN, this.TAG, `Application file ${binFile} not found.`);
            throw new Error(`Application file ${binFile} not found.`);
          }
          this.applicationSize = this.wmsg.applicationSize;
          this.currentSource = this.applicationBytes;
          hilog.info(this.DOMAIN, this.TAG, `applicationBytes: ${this.applicationSize}, ${this.applicationBytes.length}`)
          valid = true;
        }

        if (this.manifest.bootloader) {
          let binFile = this.manifest.bootloader.bin_file;
          let datFile = this.manifest.bootloader.dat_file;
          hilog.info(this.DOMAIN, this.TAG, `bootloader manifest: ${binFile}, ${datFile}`);
          //TODO: read the binFile and datFile from zip
          this.bootloaderBytes = this.wmsg.binBuf;
          this.systemInitBytes = this.wmsg.datBuf;

          if (this.bootloaderBytes == null) {
            hilog.error(this.DOMAIN, this.TAG, `Bootloader file ${binFile} not found.`);
            throw new Error(`Bootloader file ${binFile} not found.`);
          }
          this.bootloaderSize = this.wmsg.bootloaderSize;
          this.currentSource = this.bootloaderBytes;

          valid = true;
        }

        if (this.manifest.softdevice) {
          let binFile = this.manifest.softdevice.bin_file;
          let datFile = this.manifest.softdevice.dat_file;
          hilog.info(this.DOMAIN, this.TAG, `softdevice manifest: ${binFile}, ${datFile}`);
          //TODO: read the binFile and datFile from zip
          this.softDeviceBytes = this.wmsg.binBuf;
          this.systemInitBytes = this.wmsg.datBuf;

          if (this.softDeviceBytes == null) {
            hilog.error(this.DOMAIN, this.TAG, `SoftDevice file ${binFile} not found.`);
            throw new Error(`SoftDevice file ${binFile} not found.`);
          }
          this.softDeviceSize = this.wmsg.softDeviceSize;
          this.currentSource = this.softDeviceBytes;

          valid = true;
        }

        if (this.manifest.softdeviceBootloader) {
          let binFile = this.manifest.softdeviceBootloader.bin_file;
          let datFile = this.manifest.softdeviceBootloader.dat_file;
          hilog.info(this.DOMAIN, this.TAG, `softdeviceBootloader manifest: ${binFile}, ${datFile}`);
          this.softDeviceAndBootloaderBytes = this.wmsg.binBuf;
          this.systemInitBytes = this.wmsg.datBuf;

          if (this.softDeviceAndBootloaderBytes == null) {
            hilog.error(this.DOMAIN, this.TAG, `softdeviceBootloader file ${binFile} not found.`);
            throw new Error(`softdeviceBootloader file ${binFile} not found.`);
          }
          this.softDeviceSize = this.wmsg.softDeviceSize;
          this.bootloaderSize = this.wmsg.bootloaderSize;
          this.currentSource = this.softDeviceAndBootloaderBytes;

          valid = true;
        }

        if (!valid) {
          hilog.error(this.DOMAIN, this.TAG, `Manifest file must specify at least one file.`);
          throw new Error('Manifest file must specify at least one file.')
        }
      } else {
        // TODO: Compatibility mode. The 'manifest.json' file does not exist
        // ZIP file must contains one or more following files:
        // - application.hex/dat
        // 			+ application.dat
        // - softdevice.hex/dat
        // - bootloader.hex/dat
        //      + system.dat
        if (wmsg.applicationBytes) {
          wmsg.applicationSize = wmsg.applicationBytes.length;
          wmsg.currentSource = wmsg.applicationBytes;
          valid = true;
        }
        if (wmsg.bootloaderBytes) {
          wmsg.bootloaderSize = wmsg.bootloaderBytes.length;
          wmsg.currentSource = wmsg.bootloaderBytes;
          valid = true;
        }
        if (wmsg.softDeviceBytes) {
          wmsg.softDeviceSize = wmsg.softDeviceBytes.length;
          wmsg.currentSource = wmsg.softDeviceBytes;
          valid = true;
        }
      }
      fs.close(fsHandle.fd);
      if (!valid) {
        throw new Error("The ZIP file must contain an Application, a Soft Device and/or a Bootloader.");
      }
    } catch (e) {
      hilog.error(this.DOMAIN, this.TAG, `ArchiveInputStream Construct Err: ${JSON.stringify(e)}`);
    }
  }

  public available(): number {
    let ret = this.wmsg.softDeviceSize ? this.wmsg.softDeviceSize : 0 +
      this.wmsg.bootloaderSize ? this.wmsg.bootloaderSize : 0  +
      this.wmsg.applicationSize ? this.wmsg.applicationSize : 0 -
      this.bytesRead ? this.bytesRead : 0;
    hilog.info(this.DOMAIN, this.TAG, `bytesRead ${ this.bytesRead}`);
    hilog.info(this.DOMAIN, this.TAG, `applicationSize ${ this.wmsg.applicationSize}`);
    hilog.info(this.DOMAIN, this.TAG, `bootloaderSize ${ this.wmsg.bootloaderSize}`);
    hilog.info(this.DOMAIN, this.TAG, `softDeviceSize ${ this.wmsg.softDeviceSize}`);
    hilog.info(this.DOMAIN, this.TAG, `ArchiveInputStream available ${ret}`);
    return ret;
  }

  public getContentType(): number {
    this.type = 0;
    // In Secure DFU the softDeviceSize and bootloaderSize may be 0 if both are in the ZIP file.
    // The size of each part is embedded in the Init packet.
    if (this.softDeviceAndBootloaderBytes != null)
      this.type |= DfuBaseService.TYPE_SOFT_DEVICE | DfuBaseService.TYPE_BOOTLOADER;
    // In Legacy DFU the size of each of these parts was given in the manifest file.
    if (this.softDeviceSize > 0)
      this.type |= DfuBaseService.TYPE_SOFT_DEVICE;
    if (this.bootloaderSize > 0)
      this.type |= DfuBaseService.TYPE_BOOTLOADER;
    if (this.applicationSize > 0)
      this.type |= DfuBaseService.TYPE_APPLICATION;
    return this.type;
  }

  public setContentType(type: number): number {
    this.type = type;
    // If the new type has Application, but there is no application fw, remove this type bit
    if ((type & DfuBaseService.TYPE_APPLICATION) > 0 && this.applicationBytes == null)
      this.type &= ~DfuBaseService.TYPE_APPLICATION;
    // If the new type has SD+BL
    if ((type & (DfuBaseService.TYPE_SOFT_DEVICE | DfuBaseService.TYPE_BOOTLOADER)) ==
      (DfuBaseService.TYPE_SOFT_DEVICE | DfuBaseService.TYPE_BOOTLOADER)) {
      // but there is no SD, remove the softdevice type bit
      if (this.softDeviceBytes == null && this.softDeviceAndBootloaderBytes == null)
        this.type &= ~DfuBaseService.TYPE_SOFT_DEVICE;
      // or there is no BL, remove the bootloader type bit
      if (this.bootloaderBytes == null && this.softDeviceAndBootloaderBytes == null)
        this.type &= ~DfuBaseService.TYPE_SOFT_DEVICE;
    } else {
      // If at least one of SD or B: bit is cleared, but the SD+BL file is set, remove both bits.
      if (this.softDeviceAndBootloaderBytes != null)
        this.type &= ~(DfuBaseService.TYPE_SOFT_DEVICE | DfuBaseService.TYPE_BOOTLOADER);
    }

    if ((type & (DfuBaseService.TYPE_SOFT_DEVICE | DfuBaseService.TYPE_BOOTLOADER)) > 0 &&
      this.softDeviceAndBootloaderBytes != null)
      this.currentSource = this.softDeviceAndBootloaderBytes;
    else if ((type & DfuBaseService.TYPE_SOFT_DEVICE) > 0)
      this.currentSource = this.softDeviceBytes;
    else if ((type & DfuBaseService.TYPE_BOOTLOADER) > 0)
      this.currentSource = this.bootloaderBytes;
    else if ((type & DfuBaseService.TYPE_APPLICATION) > 0)
      this.currentSource = this.applicationBytes;
    this.bytesReadFromCurrentSource = 0;

    return this.type;
  }

  public fullReset() {
    // Reset stream to SoftDevice if SD and BL firmware were given separately
    if (this.softDeviceBytes != null && this.bootloaderBytes != null && this.currentSource == this.bootloaderBytes) {
      this.currentSource = this.softDeviceBytes;
    }
    // Reset the bytes count to 0
    this.bytesReadFromCurrentSource = 0;
    this.mark(0);
    this.reset();
  }

  public markSupported(): boolean {
    return true;
  }

  public getApplicationInit(): Uint8Array {
    return this.applicationInitBytes;
  }

  public getSystemInit(): Uint8Array {
    return this.systemInitBytes;
  }

  private parseZip(wmsg: WorkerMsg, mbrSize: number) {
    hilog.info(this.DOMAIN, this.TAG, `parseZip: ${mbrSize}`);
    //TODO: unzip the input file
    //If the file is hex file, need to convert to bin

    this.manifest = wmsg.mfFile?.manifest;
    this.wmsg = wmsg;
  }

  private startNextFile(): Uint8Array {
    let ret: Uint8Array;
    if (this.currentSource == this.softDeviceBytes && this.bootloaderBytes != null &&
      (this.type & DfuBaseService.TYPE_BOOTLOADER) > 0) {
      ret = this.currentSource = this.bootloaderBytes;
    } else if (this.currentSource != this.applicationBytes && this.applicationBytes != null &&
      (this.type & DfuBaseService.TYPE_APPLICATION) > 0) {
      ret = this.currentSource = this.applicationBytes;
    } else {
      ret = this.currentSource = null;
    }
    this.bytesReadFromCurrentSource = 0;
    return ret;
  }

  public async readByBuf(buffer: Uint8Array): Promise<[number, Uint8Array]> {
    return await this.readByLen(buffer, 0, buffer.length);
  }

  public async readByLen(buffer: Uint8Array, offset: number, length: number): Promise<[number, Uint8Array]> {
    let [size, buf] = await this.rawRead(buffer, offset, length);
    if (length > size && this.startNextFile() != null) {
      let [resize, rebuf] = await this.rawRead(buffer, offset + size, length - size);
      return [size+resize, rebuf]
    }
    return [size, buf];
  }

  private async rawRead(buffer: Uint8Array, offset: number, length: number): Promise<[number, Uint8Array]> {
    if (this.currentSource == null || offset < 0 || length < 0) {
      hilog.error(this.DOMAIN, this.TAG, `rawRead empty! offset[${offset}],len[${length}]}`);
      return [-1, buffer];
    }

    let maxSize: number = this.currentSource.length - this.bytesReadFromCurrentSource;
    hilog.info(this.DOMAIN, this.TAG, `rawRead maxSize: ${maxSize}}`);
    let size: number = Math.min(length, maxSize);
    // System.arraycopy(currentSource, bytesReadFromCurrentSource, buffer, offset, size);
    hilog.info(this.DOMAIN, this.TAG,
      `rawRead: ${this.currentSource.length}, ${this.bytesReadFromCurrentSource}, ${size}`);
    // buffer = this.currentSource.copyWithin(offset, this.bytesReadFromCurrentSource,
    //   this.bytesReadFromCurrentSource + size - 1);
    buffer = this.currentSource.slice(this.bytesReadFromCurrentSource, this.bytesReadFromCurrentSource + size);
    this.bytesReadFromCurrentSource += size;
    this.bytesRead += size;

    this.updateBuf = new Uint8Array([...this.updateBuf, ...buffer]);
    let checkSum: zlib.Checksum = await zlib.createChecksum();
    let crc: number = 0;
    let crcRes = await checkSum.crc32(crc, this.updateBuf.buffer);
    this.updateCrc = crcRes;

    return [size, buffer];
  }

  public mark(readlimit: number) {
    this.markedSource = this.currentSource;
    this.bytesReadFromMarkedSource = this.bytesReadFromCurrentSource;
  }

  public reset() {
    this.currentSource = this.markedSource;
    this.bytesRead = this.bytesReadFromCurrentSource = this.bytesReadFromMarkedSource;

    // Restore the CRC to the value is was on mark.
    // crc32.reset();
    if (this.currentSource == this.bootloaderBytes && this.softDeviceBytes != null) {
      // crc32.update(softDeviceBytes);
      this.bytesRead += this.softDeviceSize;
    }
    // if (currentSource != null && bytesReadFromCurrentSource > 0) {
    //   crc32.update(currentSource, 0, bytesReadFromCurrentSource);
    // }
  }

  public getBytesRead(): number {
    return this.bytesRead;
  }

  public softDeviceImageSize(): number {
    return (this.type & DfuBaseService.TYPE_SOFT_DEVICE) > 0 ? this.softDeviceSize : 0;
  }

  public applicationImageSize(): number {
    return (this.type & DfuBaseService.TYPE_APPLICATION) > 0 ? this.applicationSize : 0;
  }

  public bootloaderImageSize(): number {
    return (this.type & DfuBaseService.TYPE_BOOTLOADER) > 0 ? this.bootloaderSize : 0;
  }

  public isSecureDfuRequired(): boolean {

    let isSecureDfuRequired: boolean = false;
    if (this.manifest.bootloader != null || this.manifest.softdevice != null ||
      this.manifest.softdeviceBootloader != null) {
      isSecureDfuRequired = true;
    }
    return this.manifest != null && isSecureDfuRequired;
  }

  public async getCrc32(): Promise<number> {
    let checkSum: zlib.Checksum = await zlib.createChecksum();
    let crc: number = 0;
    let crcRes = await checkSum.crc32(crc, this.updateBuf.buffer);
    hilog.info(this.DOMAIN, this.TAG, `getCrc32: len[${this.updateBuf.length}],updateCrc[${this.updateCrc}], crcRes[${crcRes}]`);
    return crcRes;
  }
}